package nig.mf.plugin.sink;

import arc.archive.ArchiveInput;
import arc.archive.ArchiveRegistry;
import arc.archive.TableOfContentsEntry;
import arc.mf.plugin.DataSinkImpl;
import arc.mf.plugin.PluginLog;
import arc.mf.plugin.sink.ParameterDefinition;
import arc.mime.NamedMimeType;
import arc.streams.LongInputStream;
import arc.xml.XmlDoc.Element;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.Map;
import static nig.ssh.Util.illegal;


/**
 * A simple invocation logger for DataSinkImpl. Designed to help debug custom
 * sinks and the parameters supplied by its caller(s) in MediaFlux.
 * 
 * To use, create an instance inside your custom sink, and call the
 * corresponding methods on this logger when the relevant ones are invoked
 * in your implementation.
 * 
 * The logging will be performed via MediaFlux's standard {@link PluginLog}.
 * 
 * @author King Lung chiu
 */
public final class SinkLogger implements DataSinkImpl
{
   private final String TARGET_CLASS;
   private static final AtomicLong ctxCount = new AtomicLong(0);
   private boolean consumeInputStream = false;
//   private final Map<Object, TransferContext> sessions = new ConcurrentHashMap<>();
   private final Map<Object, TransferContext> sessions = new ConcurrentHashMap<Object, TransferContext>();
   


   /**
    * Indicates whether this SinkLogger's consume method will iterate through
    * all the entries in the given LongInputStream (when it's an archive).<br>
    * 
    * It's important to check this property because some archives can only
    * be traversed once. In which case, you should turn off this property via
    * {@link #consumeInputStream(boolean c)}.
    * 
    * 
    * @return
    *    Whether this SinkLogger's consume method will iterate through all
    *    entries of the given LongInputStream.
    */
   public boolean consumeInputStream() {
      return consumeInputStream;
   }
   
   /**
    * Turns on or off this logger's {@link #consumeInputStream()} property.
    * 
    * @param c
    *    Whether to turn on or off this property.
    */
   public void consumeInputStream(boolean c) {
      consumeInputStream = c;
   }
   
   public boolean sessionExists(Object ctxKey)      { return (ctxKey != null && sessions.containsKey(ctxKey)); }
   public TransferContext getSession(Object ctxKey) { return sessions.get(ctxKey); }
   
   // TODO: extract to standalone class
   // TODO: add Class<T> param & remove access to parent's TARGET_CLASS
   public final class TransferContext {
//      private final String TARGET_CLASS;
      private final long id;
      private final Map<String, String> params;
      public TransferContext(Map<String, String> params) {
         this.params = params;
         id = ctxCount.incrementAndGet();
      }
      @Override public String toString() {
         return new StringBuilder()
            .append(getClass().getCanonicalName())
            .append(String.format(" %05d", id))
            .append(" for\n                     ")
            .append(TARGET_CLASS)
            .append("\n     context params: ")
            .append(params != null? params.toString(): "(received null)")
            .toString()
         ;
      }
   }
   

   /**
    * @param name The sink class this logger instance is created for.
    */
   public SinkLogger(String name) { TARGET_CLASS = (name != null)? name: getClass().getCanonicalName(); }
   
   public void log(int level, String msg) {
      PluginLog.log(TARGET_CLASS).add(level, msg);
   }
   public void log(int level, String msg, Throwable err) {
      PluginLog.log(TARGET_CLASS).add(level, msg, err);
   }
   
   // dummy implementations: unused for now and should not be called
	@Override public String[]  acceptedTypes() throws Throwable { return null; }
	@Override public int       compressionLevelRequired()       { return -1; }
	@Override public String    description() throws Throwable   { return "Sink Logger"; }
	@Override public String    type() throws Throwable          { return "logger"; }
	@Override public Map<String, ParameterDefinition> parameterDefinitions() { return null; }


   /**
    * Begin a multi-file transfer. Use this method for session-specific
    * setups such as creation of connection objects.
    * 
    * @param params
    *    The parameters supplied to the sink.
    * 
    * @return
    *    A new {@link TransferContext} that represents this multi-file transfer
    *    session. The same object should be supplied to
    *    {@link SinkLogger#endMultiple(java.lang.Object)} when the session
    *    is complete.
    * 
    * @throws Throwable
    *    No explicit throws occur in this implementation.
    */
   @Override
   public TransferContext beginMultiple(Map<String, String> params) throws Throwable {
      log(
         PluginLog.INFORMATION,
         new StringBuilder("\nbeginMultiple(Map<String, String> params)")
            .append("\n-- params          : ")
            .append(params != null? params.toString(): "(received null)")
            .append("\n\n")
            .toString()

         
         
         );
      final TransferContext ctx = new TransferContext(params);
      sessions.put(ctx, ctx);
      return ctx;
   }

   /**
    * End a multi-file transfer. Use this method for session-specific
    * tear downs such as closure of connection objects.
    * 
    * @param ctxKey
    *    The {@link TransferContext} that represents the multi-file transfer
    *    session to tear down. This should be the same object returned from
    *    {@link SinkLogger#beginMultiple(java.util.Map)} at the start of this
    *    session.
    * 
    * @throws Throwable
    *    {@link IllegalArgumentException} when ctxKey is null.<br>
    *    {@link IllegalArgumentException} when the supplied ctxKey does not
    *    exist in this logger's internal sessions database.
    */
   @Override
   public void endMultiple(Object ctxKey) throws Throwable {
      if(ctxKey == null) illegal("null key");

      TransferContext ctx = sessions.get(ctxKey);
      log(PluginLog.INFORMATION,
         "\nendMultiple(Object ctxKey): " + (ctx != null?
            "supplied context exists\n " + ctxKey.toString() + "\n\n":
            "supplied context does NOT exist!\n\n"));
      
      if(!sessionExists(ctxKey)) {
         illegal("no such key: " + ctxKey.toString());
      } else {
         sessions.remove(ctxKey);         
      }
   }

	@Override
	public void shutdown() throws Throwable { shutdown(null); }
   public void shutdown(Callable<String> task) {
      Throwable error = null;
      StringBuilder msg = new StringBuilder("\nshutDown() ...");
      sessions.clear();

      if(task == null) {
         msg.append(" no task supplied\n");
      } else {
         msg.append(" executing supplied task ... ");
         try {
            String r = task.call();
            msg.append(" done, resultant message\n")
               .append(r)
               .append("\n");
         } catch(Throwable e) {
            msg.append(" error occurred\n");
            error = e;
         }         
      }
      
      
      if(error == null) {
         msg.append("shutdown() completed\n\n");
         log(PluginLog.INFORMATION, msg.toString());
      } else {
         msg.append("shutdown() failed\n\n");
         log(PluginLog.INFORMATION, msg.toString(), error);
      }
   }

	@Override
	public void consume(
      Object multiCtx,
      String path,
      Map<String, String> params,
      Element userMeta,
      Element meta,
      LongInputStream in,
      String appMimeType,
      String streamMimeType,
      long length
   ) throws Throwable {
      /*
       Sample output:
         consume(Object multiCtx, String path, Map<String, String> params, Element userMeta, Element meta, LongInputStream in, String appMimeType, String streamMimeType)
         -- multiCtx        : nig.test.SinkLogger.TransferContext for
          nig.test.LoggingSink
         context params:
          {baseDir=F:/zz_mf/dl, decompression=infinity}
         -- path            : 376.2.2/376.2.2.1/376.2.2.1.1/376.2.2.1.1.1/dicom_series/1_localizer\0002.dcm
         -- params          : {baseDir=F:/zz_mf/dl, decompression=infinity}
         -- userMeta        : <user id="3" domain="system" user="manager" internal="false"><root>true</root><access><administer>true</administer></access></user>
         -- userMeta\@domain: system
         -- userMeta\@user  : manager
         -- meta            : (received null)
         -- appMimeType     : content/unknown
         -- streamMimeType  : (received null)
         -- length          : 579892

         == streamMimetype  : null, input stream type unknown
                              skipping extractions of TOC and individual entries
       */
      StringBuilder msg = new StringBuilder()
         .append("\nconsume(Object multiCtx, String path, Map<String, String> params, Element userMeta, Element meta, LongInputStream in, String appMimeType, String streamMimeType)")
         
         .append("\n-- multiCtx        : ")
         .append(multiCtx != null? multiCtx.toString(): "(received null)")
         
         .append("\n-- path            : ")
         .append(path != null? path: "(received null)")
         
         .append("\n-- params          : ")
         .append(params != null? params.toString(): "(received null)")
         
         .append("\n-- userMeta        : ")
         .append(userMeta != null? userMeta.toString(): "(received null)")
         
         .append("\n-- userMeta\\@domain: ")
         .append(userMeta.attribute("domain") != null? userMeta.attribute("domain").value(): "(not available)")
         
         .append("\n-- userMeta\\@user  : ")
         .append(userMeta.attribute("user") != null? userMeta.attribute("user").value(): "(not available)")
         
         .append("\n-- meta            : ")
         .append(meta != null? meta.toString(): "(received null)")
         
         .append("\n-- appMimeType     : ")
         .append(appMimeType != null? appMimeType: "(received null)")
         
         .append("\n-- streamMimeType  : ")
         .append(streamMimeType != null? streamMimeType: "(received null)")
         
         .append("\n-- length          : ")
         .append(length)
      ;
      
      if(streamMimeType == null) {
         msg.append("\n\n== streamMimetype  : null, input stream type unknown\n");
         msg.append(    "                     skipping extractions of TOC and individual entries");
      } else if(ArchiveRegistry.isAnArchive(streamMimeType)) {
         msg.append("\n\n== streamMimetype  : archive input stream received");
            
         ArchiveInput aar = ArchiveRegistry.createInput(
            in, new NamedMimeType(streamMimeType));
         
         if(!aar.hasTableOfContents()) {
            msg.append("\n - TOC unavailable");
         } else {
            msg.append("\n - TOC:");
            TableOfContentsEntry tocEntries[] = aar.tableOfContents();
            for(TableOfContentsEntry e: tocEntries) {
               msg.append("\n   ")
                  .append(e.name());
            }
         }
         
         if(consumeInputStream) {
            msg.append("\n - individual entries:");
            ArchiveInput.Entry e = aar.next();
            while(e != null) {
               msg.append(e.isDirectory()? "\n   d:": "\n   f:")
                  .append(e.name())
               ;
               e = aar.next();
            }
         }

         aar.close();
      } else {
         msg.append("\n\n== streamMimetype  : plain inputstream received");
      }

      msg.append("\n\n");
      log(PluginLog.INFORMATION, msg.toString());

      // reset logger to not consuming input streams
      consumeInputStream = false;
	}
}